﻿/* License
 * 
 * The MIT License (MIT)
 *
 * Copyright (c) 2013, Sylvain PONTOREAU (pontoreau.sylvain@outlook.com / sylvain.pontoreau.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using System;
using System.IO;
using Xlsx.ComponentModel;
using Xlsx.DataBinder.Events;
using Xlsx.DataBinder.Extensions;
using Xlsx.DataBinder.Resources;

namespace Xlsx.DataBinder
{
    /// <summary>
    /// Bind a sheet of data into a Xlsx Document
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public sealed class XlsxDataBinder<T> where T : class
    {
        #region Event
        /// <summary>
        /// Occurs when the XlsxDataBinder binds to a data source.
        /// </summary>
        public event EventHandler DataBinding;

        /// <summary>
        /// Occurs after the XlsxDataBinder binds to a data source.
        /// </summary>
        public event EventHandler DataBound;

        /// <summary>
        /// Occurs when a data bound to row in the XlsxDataBinder.
        /// </summary>
        public event EventHandler<RowEventArgs<T>> RowDataBound;

        /// <summary>
        /// Occurs after a data bound to row in the XlsxDataBinder.
        /// </summary>
        public event EventHandler<RowEventArgs<T>> RowDataBinding;

        /// <summary>
        /// Occurs when XlsxDataBinder bound a XlsxDataSheet to a Sheet.
        /// </summary>
        public event EventHandler<XlsxDataSheetEventArgs<T>> SheetDataBinding;

        /// <summary>
        /// Occurs after XlsxDataBinder bound a XlsxDataSheet to a Sheet.
        /// </summary>
        public event EventHandler<XlsxDataSheetEventArgs<T>> SheetDataBound;
        #endregion

        #region Event raise
        /// <summary>
        /// Raise the DataBound event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnDataBound(EventArgs args)
        {
            EventHandler tmp = DataBound;

            if (tmp != null)
                tmp(this, args);
        }

        /// <summary>
        /// Raise the DataBinding event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnDataBinding(EventArgs args)
        {
            EventHandler tmp = DataBinding;

            if (tmp != null)
                tmp(this, args);
        }

        /// <summary>
        /// Raise the RowDataBound event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnRowDataBound(RowEventArgs<T> args)
        {
            EventHandler<RowEventArgs<T>> tmp = RowDataBound;

            if (tmp != null)
                tmp(this, args);
        }

        /// <summary>
        /// Raise the RowDataBinding event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnRowDataBinding(RowEventArgs<T> args)
        {
            EventHandler<RowEventArgs<T>> tmp = RowDataBinding;

            if (tmp != null)
                tmp(this, args);
        }

        /// <summary>
        /// Raise the SheetDataBound event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnSheetDataBound(XlsxDataSheetEventArgs<T> args)
        {
            EventHandler<XlsxDataSheetEventArgs<T>> tmp = SheetDataBound;

            if (tmp != null)
                tmp(this, args);
        }

        /// <summary>
        /// Raise the SheetDataBinding event
        /// </summary>
        /// <param name="args">Event arguments</param>
        private void OnSheetDataBinding(XlsxDataSheetEventArgs<T> args)
        {
            EventHandler<XlsxDataSheetEventArgs<T>> tmp = SheetDataBinding;

            if (tmp != null)
                tmp(this, args);
        }
        #endregion

        #region Properties
        /// <summary>
        /// DataSource to bind
        /// </summary>
        public XlsxDataSheet<T> DataSource { private get; set; }

        /// <summary>
        /// True if XlsxDataBinder has made the DataBind operation. 
        /// This property must be true if you want to get the byte array represent the document or to save the document.
        /// </summary>
        public bool HasDataBound { get; private set; }

        private byte[] _buffer;

        /// <summary>
        /// Get the buffer of Databound document
        /// </summary>
        public byte[] DocumentBuffer
        {
            get
            {
                if (HasDataBound)
                {
                    return _buffer;
                }
                throw new XlsxDataBinderException(XlsxException.HasNotDataBound);
            }
            private set
            {
                _buffer = value;
            }
        }
        #endregion

        /// <summary>
        /// Binds the data source into the document
        /// </summary>
        public void DataBind()
        {
            HasDataBound = false;

            if (!DataSource.IsValid())
                throw new XlsxDataBinderException(XlsxException.UnvalidDataSheet);

            OnDataBinding(EventArgs.Empty);

            var ms = new MemoryStream();

            using (var document = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook))
            {
                var se = new XlsxDataSheetEventArgs<T>(DataSource);
                OnSheetDataBinding(se);
                var workbookPart = XlsxBuilder.CreateWorkbookPart(document);
                var worksheetPart = XlsxBuilder.CreateWorksheetPart(ref workbookPart, document, DataSource.SheetId, DataSource.Name);

                uint lastRow = XlsxBuilder.AddHeader(ref worksheetPart, typeof(T));
                lastRow++;

                foreach (var data in DataSource.Data)
                {
                    var re = new RowEventArgs<T>(DataSource, data);
                    OnRowDataBinding(re);
                    XlsxBuilder.InsertRow(ref worksheetPart, lastRow, data);
                    OnRowDataBound(re);
                    lastRow++;
                }

                var workbookStylePart = workbookPart.AddNewPart<WorkbookStylesPart>("rId2");
                XlsxBuilder.ApplyStyles(ref workbookStylePart, XlsxSkinManager.XlsxHeader, XlsxSkinManager.XlsxRow, XlsxSkinManager.XlsxAlternateRow);

                OnSheetDataBound(se);

                worksheetPart.Worksheet.Save();
            }

            DocumentBuffer = ms.ToArray();

            HasDataBound = true;
            OnDataBound(EventArgs.Empty);
        }

        /// <summary>
        /// Save the document
        /// <param name="directoryPath">Directory path to save the document</param>
        /// </summary>
        public void Save(string directoryPath, string filename)
        {
            if (!HasDataBound)
                throw new XlsxDataBinderException(XlsxException.HasNotDataBound);

            FileStream stream = null;
            try
            {
                if (!Directory.Exists(directoryPath))
                    Directory.CreateDirectory(directoryPath);

                stream = new FileStream(string.Format(XlsxFormat.SavePathPattern, directoryPath, filename),
                                        FileMode.OpenOrCreate);
                stream.Write(DocumentBuffer, 0, DocumentBuffer.Length);
            }
            catch (Exception ex)
            {
                throw new XlsxDataBinderException(XlsxException.SaveFailed, ex);
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }
        }
    }
}
